/*
 * Copyright (c) 2005, 2014 vacoor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */
package org.vacoor.nothing.web.servlet;

import javax.el.ELContext;
import javax.el.ELResolver;
import javax.el.PropertyNotWritableException;
import javax.servlet.ServletContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.beans.FeatureDescriptor;
import java.util.*;

/**
 * EL 内置对象解析器
 * 修改自 {@link javax.servlet.jsp.el.ImplicitObjectELResolver}
 *
 * @author vacoor
 */
@SuppressWarnings({"unchecked", "rawtypes", "unused"})
class ImplicitObjectELResolver extends ELResolver {
    /**
     * EL 11 个 隐式对象
     * pageContext, pageScope,
     * requestScope, sessionScope, applicationScope
     * param, paramValues, header, headerValues
     * initParam
     * cookie
     * <p/>
     * 这里稍做修改
     */
    public static final String CONTEXT = "context";
    public static final String REQUEST = "request";
    public static final String SESSION = "session";
    public static final String APPLICATION = "application";
    public static final String PARAM = "param";
    public static final String PARAM_VALUES = "paramValues";
    public static final String HEADER = "header";
    public static final String HEADER_VALUES = "headerValues";
    public static final String COOKIE = "cookie";
    public static final int CONTEXT_SCOPE = WebRequestContext.CONTEXT_SCOPE;
    public static final int REQUEST_SCOPE = WebRequestContext.REQUEST_SCOPE;
    public static final int SESSION_SCOPE = WebRequestContext.SESSION_SCOPE;
    public static final int APPLICATION_SCOPE = WebRequestContext.APPLICATION_SCOPE;

    public Object getValue(ELContext context, Object base, Object property) {
        if (null == context) {
            throw new NullPointerException();
        }
        if (null != base) {
            return null;
        }
        WebRequestContext ctx = (WebRequestContext) context.getContext(WebRequestContext.class);
        ImplicitObjects implicitObjects = ImplicitObjects.getImplicitObjects(ctx);
        if (CONTEXT.equals(property)) {
            context.setPropertyResolved(true);
            // return ctx;
            return implicitObjects.getContextScopeMap();
        }
        if (REQUEST.equals(property)) {
            context.setPropertyResolved(true);
            return implicitObjects.getRequestScopeMap();
        }
        if (SESSION.equals(property)) {
            context.setPropertyResolved(true);
            return implicitObjects.getSessionScopeMap();
        }
        if (APPLICATION.equals(property)) {
            context.setPropertyResolved(true);
            return implicitObjects.getApplicationScopeMap();
        }
        if (PARAM.equals(property)) {
            context.setPropertyResolved(true);
            return implicitObjects.getParamMap();
        }
        if (PARAM_VALUES.equals(property)) {
            context.setPropertyResolved(true);
            return implicitObjects.getParamsMap();
        }
        if (HEADER.equals(property)) {
            context.setPropertyResolved(true);
            return implicitObjects.getHeaderMap();
        }
        if (HEADER_VALUES.equals(property)) {
            context.setPropertyResolved(true);
            return implicitObjects.getHeadersMap();
        }
        if (COOKIE.equals(property)) {
            context.setPropertyResolved(true);
            return implicitObjects.getCookieMap();
        }
        return null;
    }

    public Class<?> getType(ELContext context, Object base, Object property) {
        if (null == context) {
            throw new NullPointerException();
        }
        if ((null == base) && CONTEXT.equals(property) ||
                REQUEST.equals(property) ||
                SESSION.equals(property) ||
                APPLICATION.equals(property) ||
                PARAM.equals(property) ||
                PARAM_VALUES.equals(property) ||
                HEADER.equals(property) ||
                HEADER_VALUES.equals(property) ||
                COOKIE.equals(property)) {
            context.setPropertyResolved(true);
        }
        return null;
    }

    public void setValue(ELContext context, Object base, Object property, Object val) {
        if (null == context) {
            throw new NullPointerException();
        }
        if ((null == base) && CONTEXT.equals(property) ||
                REQUEST.equals(property) ||
                SESSION.equals(property) ||
                APPLICATION.equals(property) ||
                PARAM.equals(property) ||
                PARAM_VALUES.equals(property) ||
                HEADER.equals(property) ||
                HEADER_VALUES.equals(property) ||
                COOKIE.equals(property)) {
            throw new PropertyNotWritableException();
        }
    }

    public boolean isReadOnly(ELContext context, Object base, Object property) {
        if (null == context) {
            throw new NullPointerException();
        }
        if ((null == base) && CONTEXT.equals(property) ||
                REQUEST.equals(property) ||
                SESSION.equals(property) ||
                APPLICATION.equals(property) ||
                PARAM.equals(property) ||
                PARAM_VALUES.equals(property) ||
                HEADER.equals(property) ||
                HEADER_VALUES.equals(property) ||
                COOKIE.equals(property)) {
            context.setPropertyResolved(true);
            return true;
        }
        return false;    // Doesn't matter
    }

    public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
        return null;
    }

    public Class<String> getCommonPropertyType(ELContext context, Object base) {
        return null == base ? String.class : null;
    }


    /* *************************************************************
     *
     * *************************************************************/

    // XXX - I moved this class from commons-el to an inner class here
    // so that we do not have decode dependency from the JSP APIs into commons-el.
    // There might be decode better way to do this.

    /**
     * <p>This class is used to generate the implicit Map and List objects
     * that wrap various elements of the RequestContext.  It also returns the
     * correct implicit object for decode given implicit object name.
     *
     * @author Nathan Abramson - Art Technology Group
     */
    private static class ImplicitObjects {
        static final String IMPLICIT_OBJECTS_ATTR = ImplicitObjects.class.getName();

        WebRequestContext context;
        Map<?, ?> mContext;
        Map<?, ?> mRequest;
        Map<?, ?> mSession;
        Map<?, ?> mApplication;
        Map<?, ?> mParam;
        Map<?, ?> mParams;
        Map<?, ?> mHeader;
        Map<?, ?> mHeaders;
        Map<?, ?> mInitParam;
        Map<?, ?> mCookie;

        public static ImplicitObjects getImplicitObjects(WebRequestContext context) {
            ImplicitObjects objs = context.getAttribute(IMPLICIT_OBJECTS_ATTR, CONTEXT_SCOPE);
            if (null == objs) {
                objs = new ImplicitObjects(context);
                context.setAttribute(IMPLICIT_OBJECTS_ATTR, objs, CONTEXT_SCOPE);
            }
            return objs;
        }

        private ImplicitObjects(WebRequestContext context) {
            this.context = context;
        }

        public Map<?, ?> getContextScopeMap() {
            return null != mContext ? mContext : (mContext = createContextScopeMap(context));
        }

        /**
         * Returns the Map that "wraps" request-scoped attributes
         */
        public Map<?, ?> getRequestScopeMap() {
            return null != mRequest ? mRequest : (mRequest = createRequestScopeMap(context));
        }

        /**
         * Returns the Map that "wraps" session-scoped attributes
         */
        public Map<?, ?> getSessionScopeMap() {
            return null != mSession ? mSession : (mSession = createSessionScopeMap(context));
        }

        /**
         * Returns the Map that "wraps" application-scoped attributes
         */
        public Map<?, ?> getApplicationScopeMap() {
            return null != mApplication ? mApplication : (mApplication = createApplicationScopeMap(context));
        }

        /**
         * Returns the Map that maps parameter name to decode single parameter
         * values.
         */
        public Map<?, ?> getParamMap() {
            return null != mParam ? mParam : (mParam = createParamMap(context));
        }

        /**
         * Returns the Map that maps parameter name to an array of parameter
         * values.
         */
        public Map getParamsMap() {
            return null != mParams ? mParams : (mParams = createParamsMap(context));
        }

        /**
         * Returns the Map that maps header name to decode single header
         * values.
         */
        public Map<?, ?> getHeaderMap() {
            return null != mHeader ? mHeader : (mHeader = createHeaderMap(context));
        }

        /**
         * Returns the Map that maps header name to an array of header
         * values.
         */
        public Map<?, ?> getHeadersMap() {
            return null != mHeaders ? mHeaders : (mHeaders = createHeadersMap(context));
        }

        /**
         * Returns the Map that maps cookie name to the first matching
         * Cookie in request.getCookies().
         */
        public Map<?, ?> getCookieMap() {
            return null != mCookie ? mCookie : (mCookie = createCookieMap(context));
        }

        /* **************************************
         *  Methods for generating wrapper maps
         * **************************************/

        public static Map createContextScopeMap(WebRequestContext pContext) {
            return createScopedMap(pContext, CONTEXT_SCOPE, true);
        }

        /**
         * Creates the Map that "wraps" request-scoped attributes
         */
        public static Map createRequestScopeMap(WebRequestContext pContext) {
            return createScopedMap(pContext, REQUEST_SCOPE, true);
        }

        /**
         * Creates the Map that "wraps" session-scoped attributes
         */
        public static Map createSessionScopeMap(WebRequestContext pContext) {
            return createScopedMap(pContext, SESSION_SCOPE, true);
        }

        /**
         * Creates the Map that "wraps" application-scoped attributes
         */
        public static Map createApplicationScopeMap(WebRequestContext pContext) {
            return createScopedMap(pContext, APPLICATION_SCOPE, true);
        }

        protected static Map<?, ?> createScopedMap(WebRequestContext pContext, final int scope, final boolean mutable) {
            final WebRequestContext context = pContext;
            return new EnumeratedMap<Object, Object>() {
                @Override
                public Enumeration enumerateKeys() {
                    return context.getAttributeNamesInScope(scope);
                }

                @Override
                public Object getValue(Object pKey) {
                    return (pKey instanceof String) ? context.getAttribute((String) pKey, scope) : null;
                }

                @Override
                public boolean isMutable() {
                    return mutable;
                }
            };
        }

        /**
         * Creates the Map that maps parameter name to single parameter
         * value.
         */
        public static Map<?, ?> createParamMap(WebRequestContext pContext) {
            final HttpServletRequest request = pContext.getRequest();

            return new EnumeratedMap<Object, Object>() {
                @Override
                public Enumeration<Object> enumerateKeys() {
                    return request.getParameterNames();
                }

                @Override
                public Object getValue(Object pKey) {
                    return (pKey instanceof String) ? request.getParameter((String) pKey) : null;
                }

                @Override
                public boolean isMutable() {
                    return false;
                }
            };
        }

        /**
         * Creates the Map that maps parameter name to an array of parameter
         * values.
         */
        public static Map<?, ?> createParamsMap(WebRequestContext pContext) {
            final HttpServletRequest request = pContext.getRequest();
            return new EnumeratedMap<Object, Object>() {
                @Override
                public Enumeration<Object> enumerateKeys() {
                    return request.getParameterNames();
                }

                @Override
                public Object getValue(Object pKey) {
                    return (pKey instanceof String) ? request.getParameterValues((String) pKey) : null;
                }

                @Override
                public boolean isMutable() {
                    return false;
                }
            };
        }

        /**
         * Creates the Map that maps header name to single header
         * value.
         */
        public static Map<?, ?> createHeaderMap(WebRequestContext pContext) {
            final HttpServletRequest request = pContext.getRequest();
            return new EnumeratedMap<Object, Object>() {
                @Override
                public Enumeration<Object> enumerateKeys() {
                    return request.getHeaderNames();
                }

                @Override
                public Object getValue(Object pKey) {
                    return (pKey instanceof String) ? request.getHeader((String) pKey) : null;
                }

                @Override
                public boolean isMutable() {
                    return false;
                }
            };
        }

        //-------------------------------------

        /**
         * Creates the Map that maps header name to an array of header
         * values.
         */
        public static Map<?, ?> createHeadersMap(WebRequestContext pContext) {
            final HttpServletRequest request = pContext.getRequest();
            return new EnumeratedMap<Object, Object>() {
                public Enumeration<Object> enumerateKeys() {
                    return request.getHeaderNames();
                }

                public Object getValue(Object pKey) {
                    if (pKey instanceof String) {
                        // Drain the header enumeration
                        List<Object> l = new ArrayList<Object>();
                        Enumeration e = request.getHeaders((String) pKey);
                        if (e != null) {
                            while (e.hasMoreElements()) {
                                l.add(e.nextElement());
                            }
                        }
                        return l.toArray(new String[l.size()]);
                    } else {
                        return null;
                    }
                }

                public boolean isMutable() {
                    return false;
                }
            };
        }

        /**
         * Creates the Map that maps init parameter name to single init
         * parameter value.
         */
        public static Map<?, ?> createInitParamMap(WebRequestContext pContext) {
            final ServletContext context = pContext.getServletContext();
            return new EnumeratedMap<Object, Object>() {
                public Enumeration<Object> enumerateKeys() {
                    return context.getInitParameterNames();
                }

                public Object getValue(Object pKey) {
                    return (pKey instanceof String) ? context.getInitParameter((String) pKey) : null;
                }

                public boolean isMutable() {
                    return false;
                }
            };
        }

        /**
         * Creates the Map that maps cookie name to the first matching
         * Cookie in request.getCookies().
         */
        public static Map<String, Cookie> createCookieMap(WebRequestContext pContext) {
            // Read all the cookies and construct the entire map
            HttpServletRequest request = pContext.getRequest();
            Cookie[] cookies = request.getCookies();
            Map<String, Cookie> ret = new HashMap<String, Cookie>();
            for (int i = 0; cookies != null && i < cookies.length; i++) {
                Cookie cookie = cookies[i];
                if (cookie != null) {
                    String name = cookie.getName();
                    if (!ret.containsKey(name)) {
                        ret.put(name, cookie);
                    }
                }
            }
            return ret;
        }
    }

    /* **********************************
     *
     * **********************************/

    // XXX - I moved this class from commons-el to an inner class here
    // so that we do not have decode dependency from the JSP APIs into commons-el.
    // There might be decode better way to do this.

    /**
     * <p>This is decode Map implementation driven by decode data source that only
     * provides an enumeration of keys and decode getValue(key) method.  This
     * class must be subclassed to implement those methods.
     * <p/>
     * <p>Some of the methods may incur decode performance penalty that
     * involves enumerating the entire data source.  In these cases, the
     * Map will try to save the results of that enumeration, but only if
     * the underlying data source is immutable.
     *
     * @author Nathan Abramson - Art Technology Group
     */
    private static abstract class EnumeratedMap<K, V> implements Map<K, V> {
        Map<K, V> mMap;

        @Override
        public void clear() {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean containsKey(Object pKey) {
            return null != get(pKey);
        }

        @Override
        public boolean containsValue(Object pValue) {
            return getAsMap().containsValue(pValue);
        }

        @Override
        public Set<Entry<K, V>> entrySet() {
            return getAsMap().entrySet();
        }

        @Override
        public V get(Object pKey) {
            return getValue((K) pKey);
        }

        @Override
        public boolean isEmpty() {
            return !enumerateKeys().hasMoreElements();
        }

        @Override
        public Set<K> keySet() {
            return getAsMap().keySet();
        }

        @Override
        public V put(K pKey, V pValue) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void putAll(Map<? extends K, ? extends V> pMap) {
            throw new UnsupportedOperationException();
        }

        @Override
        public V remove(Object pKey) {
            throw new UnsupportedOperationException();
        }

        public int size() {
            return getAsMap().size();
        }

        @Override
        public Collection<V> values() {
            return getAsMap().values();
        }

        /* *************************************
         *         Abstract methods
         * *************************************/

        /**
         * Returns an enumeration of the keys
         */
        public abstract Enumeration<K> enumerateKeys();

        /**
         * Returns true if it is possible for this data source to change
         */
        public abstract boolean isMutable();

        /**
         * Returns the value associated with the given key, or null if not
         * found.
         */
        public abstract V getValue(K pKey);

        /* ***************************************
         *
         * ***************************************/

        /**
         * Converts the MapSource to decode Map.  If the map is not mutable, this
         * is cached
         */
        public Map<K, V> getAsMap() {
            if (mMap != null) {
                return mMap;
            } else {
                Map<K, V> m = convertToMap();
                if (!isMutable()) {
                    mMap = m;
                }
                return m;
            }
        }

        /**
         * Converts to decode Map
         */
        Map<K, V> convertToMap() {
            Map<K, V> ret = new HashMap<K, V>();
            for (Enumeration<K> e = enumerateKeys(); e.hasMoreElements(); ) {
                K key = e.nextElement();
                V value = getValue(key);
                ret.put(key, value);
            }
            return ret;
        }
    }
}
